import re

from django.db import models
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.db.models.signals import pre_save, post_save
from django.dispatch import receiver

from django.contrib.auth.models import User


#
# TODO:
#   * Use an event for creation in addition of Historyfied create_date 
#   So that the event thread gives all informations w/o he need to read
#   the Historyfied instance 
#
#
#



# A regexp for all names:
_FileNameRe = re.compile(r'^[a-zA-Z][a-zA-Z0-9]+$')

# Create your models here.
#class Post(models.Model):
#    author = models.ForeignKey(User)
#    title = models.CharField(max_length=30)
#    text = models.TextField()
#    date = models.DateTimeField()
#    
#    def __unicode__(self):
#        return self.title

class Status(models.Model):
    name = models.CharField(max_length=32)
    after = models.ForeignKey('self', null=True, blank=True)
    
    def __unicode__(self):
        return self.name

class Event(models.Model):
    date = models.DateTimeField(auto_now_add=True)
    user = models.ForeignKey(User, related_name='event_user')
    model = models.CharField(max_length=16)
    model_id = models.IntegerField()
    model_field = models.CharField(max_length=32, default='_All_')
    previous_value = models.CharField(max_length=256, default='_None_')
    new_value = models.CharField(max_length=256)
    description = models.CharField(max_length=128, default='Creation')
    previous_event = models.ForeignKey('self', null=True, blank=True)

    def __unicode__(self):
        return self.description
    
class Historyfied(models.Model):
    '''
    This base model ensures subclasses have the required fields
    and behavior to store all modifications in the Event table.
    
    It is used as a base for all models heres.
    
    All sub class MUST be decorated with the @Historyfied.BindEvents decorator.
    See Historyfied.BindEvents's doc.
    
    '''
    name_validator = RegexValidator(
        _FileNameRe,
        "Bad name, must start with a letter and use only letters, digit and underscores",
    )
    name = models.CharField(max_length=32, unique=True, validators=[name_validator])
    
    create_date = models.DateTimeField(auto_now_add=True)
    edit_date = models.DateTimeField(auto_now=True)
    last_event = models.ForeignKey(Event, null=True, blank=True)
    
    def _field_names(self):
        return [ f.name for f in self._meta.fields ]
    
    @classmethod
    def _pre_save(klass, sender, instance, using, **kwargs):
        '''
        Pre save hook for Historyfied and subclass instances.
        Will compute the difference between the instance to save and the db state
        and create an Event entry for each of them in order to set the instance's
        last_event field.
        '''
        print "########## Historyfied PRE-SAVE on", str(instance), `instance.pk`
        
        if instance.pk == None:
            print '    Creation Time, no diff to find out.'
            return
            
        previous = instance.__class__.objects.get(pk=instance.pk)
            
        diffs = []
        for fieldName in instance._field_names():
            print '   %20s' % fieldName, 
            if fieldName.startswith('_') or fieldName.endswith('_ptr'):
                print 'skipped'
                continue
            try:
                pv = getattr(previous,fieldName)
            except AttributeError, err:
                print err
                continue
            iv = getattr(instance,fieldName)
            
            if pv != iv:
                description = "Changing "+sender.__name__+"'s "+fieldName+' from "'+str(pv)+'" to "'+str(iv)+'"'
                print description
                diffs.append(
                    {
                        'model_field':fieldName, 
                        'previous_value':pv,
                        'new_value':iv,
                        'description':description,
                    }
                )
            else:
                print "untouched"
                
        last_event = previous.last_event
        for diff in diffs:
            e = Event(
                    user=User.objects.get(pk=1),                # TODO: get current user
                    model=instance.__class__.__name__,
                    model_id=instance.pk,
                    previous_event=last_event,
                    **diff
                )
            e.save()
            last_event = e
        instance.last_event = last_event

    @staticmethod
    def BindEvents(klass):
        '''
        This decorator must be used on every subclasses to propagate the events
        to ancestor classes. This is needed because we filter events on the class name
        (Which changes when subclassing :p)
        
        The subclass may override the _pre_save class method if it calls its super class'
        implementation in its own implementation:
            @classmethod
            def _pre_save(klass, sender, instance, using, **kwargs):
                super(MySubClass, klass)._pre_save(sender, instance, using, **kwargs)
                # do my stuffs...
                
        '''
        if not issubclass(klass, Historyfied):
            raise Exception(
                "Bad decorator usage on "+`klass`+", allowed on Historyfied sub classes only."
            )
        pre_save.connect(klass._pre_save, sender=klass, dispatch_uid=klass.__name__+"._pre_save")
        return klass
        

@Historyfied.BindEvents        
class Tracked(Historyfied):
    '''
    This Model is herited to add specific Tracked fields.
    Use the Cast and As* methods to cast your object to the desired type.
    
    As a Django restriction, every relational fields in subclasses
    will have to give the 'related_name' keyword argument.

    '''
    # Possible values for the type field.
    # This field is used to cast a Tracked instance to the
    # actual Model class.
    TYPES = (
        # DBValue,   DisplayValue
        ('Project',  'Project'),
        ('Lib',      'Library'),
        ('Entity',   'Entity'),
        ('Sequence', 'Sequence'),
        ('Shot',     'Shot'),
    )
    
    depend = models.ForeignKey('self', null=True, blank=True)
    _type = models.CharField(max_length=32, choices=TYPES, blank=True) # Class name of this component.
    priority = models.PositiveSmallIntegerField(default=50,blank=True)
    start_date = models.DateTimeField(null=True,blank=True)
    due_date = models.DateTimeField(null=True,blank=True)
    status = models.ForeignKey(Status)
    
    def __unicode__(self):
        return self._type+': '+self.name
    
    def Cast(self):
        return getattr(self,self._type.lower())
    
    def As(self, type):
        if self._type != type:
            return None
        return getattr(self,type.lower())
        
    def AsProject(self):
        return self.As('Project')
    
    def AsLib(self):
        return self.As('Lib')
    
    def AsEntity(self):
        return self.As('Entity')
    
    def AsSequence(self):
        return self.As('Sequence')
    
    def AsShot(self):
        return self.As('Shot')

    @classmethod
    def _pre_save(klass, sender, instance, using, **kwargs):
        '''
        Call the base class _pre_save as required by the Historyfied
        ancestor class and set the _type field value if needed.
        '''
        super(Tracked, klass)._pre_save(sender, instance, using, **kwargs)
        print "########## Tracked "+klass.__name__+" PRE-SAVE for _type field"
        if not instance._type:
            print '   using', `klass.__name__`
            instance._type=klass.__name__
        
class ProjectManager(models.Manager):
    def get_type(self,type):
        l = super(ProjectManager, self).get_query_set().filter(type=type)
        return [i.Cast() for i in l]
    
    def libs(self):
        return self.get_type('Lib')
    
    def entities(self):
        return self.get_type('Entities')
    
    def sequences(self):
        return self.get_type('Sequence')
    
    def shots(self):
        return self.get_type('Shot')

@Historyfied.BindEvents
class Project(Tracked):
    label = models.CharField(max_length=64)
    desc = models.TextField()
    
    components = ProjectManager() # additional manager
    
    def __unicode__(self):
        return self.name

@Historyfied.BindEvents
class ProjectComponent(Tracked):
    '''
    This Model is a base for all Model belonging to a Project.
    '''
    project = models.ForeignKey(Project, related_name='components')
    variantName = models.CharField(
        max_length=32,
        blank=True,
        help_text="""\
The variant is an extension of the name.
Two items with the same name and a different variantName are really distincts, but relate to the 
same topic."""
    )
    
    def __unicode__(self):
        return self._type+': '+self.name+(self.variantName and ' ('+self.variantName+')' or '')
    
    def GetAsset(departement, types, eye=None, lod=None, stage=None):
        # and the asset would have GetFile( extension, suffix=None )
        raise Exception("This should be available only on tasks...")
    

@Historyfied.BindEvents
class Post(Historyfied):
    '''
    Posts can be related to a project component and optionnally have a parent Post.
    This forms a threaded forum with component topic.
    '''
    component = models.ForeignKey(ProjectComponent, blank=True)
    parent = models.ForeignKey('self', blank=True)
    subject = models.CharField(max_length=64)
    content = models.TextField()
    subscribers = models.ManyToManyField(User, blank=True, related_name='post_subscribers')
    
    def __unicode__(self):
        return self.name

@Historyfied.BindEvents
class Lib(ProjectComponent):
    desc = models.TextField()
    
@Historyfied.BindEvents
class Entity(ProjectComponent):
    '''
    Project Entities can be typed with As* functions.
    '''
    TYPES = (
        ('CHAR','CHAR'),
        ('SET','SET'),
        ('PROP','PROP'),
    )
    ent_type = models.CharField(max_length=16, choices=TYPES) # Class name of this Entity
    lib = models.ForeignKey(Lib, related_name='entities')

    hasHair = models.BooleanField()
    hasCloth = models.BooleanField()
    
    def __unicode__(self):
        return '_'.join((self.lib.name,self.ent_type or 'other',self.name))
    
    def CastEnt(self):
        return getattr(self,self.ent_type.lower())
        
    def AsEnt(self, type):
        if self._type != type:
            return None
        return getattr(self,type.lower())
        
    def AsChar(self):
        return self.AsEnt('Char')

    def AsSet(self):
        return self.AsEnt('Set')

    def AsProp(self):
        return self.AsEnt('Prop')

    @classmethod
    def _pre_save(klass, sender, instance, using, **kwargs):
        '''
        Call the base class _pre_save as required by the Historyfied
        ancestor class and set the ent_type field value if needed.
        '''
        super(Entity, klass)._pre_save(sender, instance, using, **kwargs)
        print "########## Entity "+klass.__name__+" PRE-SAVE for ent_type and project fields"
        if not instance._type:
            print '   using ent_type', `klass.__name__`
            instance.ent_type=klass.__name__

@Historyfied.BindEvents
class Char(Entity):
    FAMILIES = (
        ('Main','Main'),
        ('Secondary','Secondary'),
        ('Tertiary','Tertiary'),
        ('Crowd','Crowd'),
    )
    family = models.CharField(
        max_length=32,
        choices=FAMILIES,
        help_text='Use the family to classify Characters.'
    )

@Historyfied.BindEvents
class Set(Entity):
    location = models.CharField(
        max_length=32,
        help_text='The location is used to group some related/connected Sets'
    )

@Historyfied.BindEvents
class Prop(Entity):
    group = models.CharField(
        max_length=32,
        help_text='You may use "/" in groups to create nested groups.'
    )

@Historyfied.BindEvents
class Sequence(ProjectComponent):
    order = models.PositiveSmallIntegerField(default=0)

@Historyfied.BindEvents
class Shot(Tracked):
    sequence = models.ForeignKey(Sequence, related_name='shots')
    order = models.PositiveSmallIntegerField(default=0)
    


